/*
 * Copyright (C) 2007 Luca Veltri - University of Parma - Italy
 *
 * This file is part of MjSip (http://www.mjsip.org)
 *
 * MjSip is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * MjSip is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MjSip; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Author(s):
 * Luca Veltri (luca.veltri@unipr.it)
 */

package org.zoolu.sip.provider;


import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import org.zoolu.net.IpAddress;
import org.zoolu.sip.address.SipURL;
import org.zoolu.sip.header.ViaHeader;
import org.zoolu.sip.message.Message;
import org.zoolu.tools.*;

import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;



/** SipProvider implements the SIP transport layer, that is the layer responsible for
 * sending and receiving SIP messages.
 * <p/>
 * Any SipProvider user can send SIP messages through method <i>sendMessage()</i>
 * and receive incoming SIP messages by setting itself as message listener through the method
 * <i>addSelectiveListener()</i> (specifying also the type of messages it wants to listen to)
 * and by implementing the callback method <i>onReceivedMessage()</i> defined by the interface
 * SipProviderListener.
 * <p/>
 * if a SipProvider user wants to capture ALL received message regardless of any other
 * concurrent listening users, it may use the <i>addPromiscuousListener()<i/> method.
 * <p/>
 * SipProvider implements also multiplexing/de-multiplexing service allowing a listener
 * to be bound to a specific type of SIP messages through the
 * addSelectiveListener(<i>id<i/>,<i>listener<i/>) method, where:
 * <b> - <i>id<i/> is the SIP identifier the listener has to be bound to,
 * <b> - <i>listener<i/> is the SipProviderListener that received messages are passed to.
 * <p/>
 * The SIP identifier specifies the type of messages that the listener is going to
 * receive for. Together with the SipProvider transport protocols, port numbers and
 * IP addresses, it represents the complete SIP Service Access Point (SAP) address/identifier
 * at receiving side.
 * <p/>
 * The identifier can be of one of the three following types: transaction_id, dialog_id,
 * or method_id. These types of identifiers characterize respectively:
 * <br/> - messages within a specific transaction,
 * <br/> - messages within a specific dialog,
 * <br/> - messages related to a specific SIP method.
 * It is also possible to use the the identifier ANY to specify
 * <br/> - all messages that are out of any transactions, dialogs, or method types
 *         already specified.
 * <p/>
 * When receiving a message, the SipProvider passes the message to any active listeners
 * added in promiscuous mode thorugh method <i>addPromiscuousListener()</i>.
 * Then the message is passed to the eventual selective listeners added
 * thorugh method <i>addSelectiveListener()</i> and matching the given message.
 * For this purpose the SipProvider first tries to look for a matching
 * transaction id, then looks for a matching dialog id, then for a matching method id,
 * and finally for a default listener (i.e. that with id "ANY").
 * For the matched SipProviderListener, the method <i>onReceivedMessage()</i> is fired.
 * <p/>
 * Note: no 482 (Loop Detected) nor 501 (Not Implemented) responses are generated for requests
 * that does not properly match any active transactions, dialogs, nor method types.
 */
public class SipProvider implements Configurable, TransportListener {

    private static final String TAG = "Sip:SipProvider";
    private static final String SDP_TAG = "Sip:SDP";

    // **************************** Constants ****************************

    /** UDP protocol type */
    static final String PROTO_UDP="udp";
    /** TCP protocol type */
    static final String PROTO_TCP="tcp";
    /** TLS protocol type */
    private static final String PROTO_TLS="tls";
    /** SCTP protocol type */
    public static final String PROTO_SCTP="sctp";

    /** String value "auto-configuration" used for auto configuration of the host address. */
    private static final String AUTO_CONFIGURATION="AUTO-CONFIGURATION";

    /** String value "auto-configuration" used for auto configuration of the host address. */
    private static final String ALL_INTERFACES="ALL-INTERFACES";

    /** Identifier used as listener id for capturing ANY incoming messages
     * that does not match any active method_id, transaction_id, nor dialog_id.
     * <br/> In this context, "active" means that there is a active listener
     * for that specific method, transaction, or dialog. */
    public static final SipId ANY=new SipId("ANY");

    /** Minimum length for a valid SIP message.  */
    private static final int MIN_MESSAGE_LENGTH=12;


    // ********************* Configurable attributes *********************

    /** Via IP address or fully-qualified domanin rtpmap (FQDN).
     * Use 'auto-configuration' for auto detection, or let it undefined. */
    private String via_addr = null;

    /** Local SIP port */
    private int host_port = 0;

    /** Network interface (IP address) used by SIP.
     * Use 'ALL-INTERFACES' for binding SIP to all interfaces (or let it undefined). */
    private String host_ifaddr = null;

    /** List of enabled transport protocols (the first protocol is used as default). */
    private String[] transport_protocols = null;

    /** List of transport ports, ordered as the corresponding transport_protocols. */
    private int[] transport_ports = null;

    /** Max number of (contemporary) open connections */
    private int nmax_connections = 0;

    /** Outbound proxy URL ([sip:]host_addr[:host_port][;transport=proto]).
     * Use 'NONE' for not using an outbound proxy (or let it undefined). */
    private SipURL outbound_proxy = null;

    /** Whether logging all packets (including non-SIP keepalive tokens). */
    private boolean log_all_packets=false;


    /** For TLS. Whether all client and server certificates should be considered trusted.
     * By default, trust_all=false. */
    private boolean trust_all=false;

    /** For TLS. Path of the folder where trusted certificates are placed.
     * All certificates (with file extension ".crt") found in this folder are considered trusted.
     * By default, the folder "./cert" is used. */
    private String trust_folder="cert";

    /** For TLS. Absolute file rtpmap of the certificate (containing the public key) of the local node.
     * The file rtpmap includes the full path starting from the current working folder.
     * By default, the file "./cert/ssl.crt" is used. */
    private String cert_file="cert/ssl.crt";

    /** For TLS. Absolute file rtpmap of the private key of the local node.
     * The file rtpmap includes the full path starting from the current working folder.
     * By default, the file "./cert/ssl.key" is used. */
    private String key_file="cert/ssl.key";


    // for backward compatibility:

    /** Outbound proxy addr (for backward compatibility). */
    private String outbound_addr=null;
    /** Outbound proxy port (for backward compatibility). */
    private int outbound_port=-1;


    // ************************ Other attributes *************************

    /** Network interface (IP address) used by SIP. */
    private IpAddress host_ipaddr=null;

    /** Table of supported transport layers for SIP, as table:(String)protocol-->(Transport)transport. */
    private Hashtable sip_transports=null;

    /** Default transport */
    private String default_transport=null;

    /** Whether adding 'rport' parameter on outgoing requests. */
    private boolean rport=true;

    /** Whether forcing 'rport' parameter on incoming requests ('force-rport' mode). */
    private boolean force_rport=false;

    /** Table of sip listeners as Hashtable:(SipId)id-->(SipProviderListener)listener */
    private Hashtable sip_listeners=new Hashtable();

    /** Vector of promiscuous listeners */
    private Vector promiscuous_listeners=new Vector();

    /** Vector of exception listeners */
    private Vector exception_listeners=new Vector();



    // *************************** Costructors ***************************


    /** Creates a new SipProvider.
     * Costructs the SipProvider, initializing the SipProviderListeners, the transport protocols, and other attributes. */
    public SipProvider(String via_addr, int host_port, Transport[] sip_transports, Context context) {
        init(via_addr,host_port, context);

        // init transport
        this.sip_transports=new Hashtable();
        if (sip_transports!=null) {
            for (Transport sip_transport : sip_transports) setTransport(sip_transport);
            if (sip_transports.length>0) default_transport=sip_transports[0].getProtocol();
        }
    }


    /** Creates a new SipProvider.
     * The SipProvider attributres are read from file. */
    public SipProvider(Context context) {

        if (!SipStack.isInit())
            SipStack.init(context);

        readAll(context);
        init(via_addr, host_port, context);
        initSipTrasport(transport_protocols, transport_ports, host_ifaddr);
    }


    /** Inits the SipProvider, initializing the SipProviderListeners, the transport protocols, the outbound proxy, and other attributes. */
    private void init(String via_addr, int host_port, Context context) {
        if (!SipStack.isInit()) SipStack.init(context);
        if (via_addr == null || via_addr.equalsIgnoreCase(AUTO_CONFIGURATION)) {
            via_addr = IpAddress.getLocalHostAddress().toString();
        }

        this.via_addr = via_addr;
        if (host_port<=0) host_port=SipStack.default_port;
        this.host_port=host_port;
        rport=SipStack.use_rport;
        force_rport=SipStack.force_rport;

        // just for backward compatibility..
        if (outbound_port<0) outbound_port=SipStack.default_port;
        if (outbound_addr!=null)
        {  if (outbound_addr.equalsIgnoreCase("NONE") || outbound_addr.equalsIgnoreCase("NO-OUTBOUND")) outbound_proxy=null;
        else outbound_proxy=new SipURL(outbound_addr,outbound_port);
        }
    }

    /** Inits and starts the transport services. */
    private void initSipTrasport(String[] transport_protocols, int[] transport_ports, String ifaddr)
    {
        host_ipaddr=null;
        if (ifaddr!=null && !ifaddr.equalsIgnoreCase(ALL_INTERFACES))
        {  try {  host_ipaddr=IpAddress.getByName(ifaddr);  } catch (IOException e) {  e.printStackTrace(); host_ipaddr=null;  }
        }
        if (transport_protocols==null) transport_protocols=SipStack.default_transport_protocols;
        this.transport_protocols=transport_protocols;
        if (transport_protocols.length>0) default_transport=transport_protocols[0];
        if (nmax_connections<=0) nmax_connections=SipStack.default_nmax_connections;

        sip_transports=new Hashtable();
        for (int i=0; i<transport_protocols.length; i++)
        {  try
        {  String proto=transport_protocols[i].toLowerCase();
            int port=(transport_ports!=null && transport_ports.length>i)? port=transport_ports[i] : 0;
            Transport transp=null;
            if (proto.equals(PROTO_UDP))
            {  if (port==0) port=host_port;
                transp=new UdpTransport(port,host_ipaddr);
            }
            else
            if (proto.equals(PROTO_TCP))
            {  if (port==0) port=host_port;
                transp=new TcpTransport(port,host_ipaddr,nmax_connections);
            }
            else
            if (proto.equals(PROTO_TLS))
            {  if (port==0) port=(host_port==SipStack.default_port)? SipStack.default_tls_port : host_port;
                transp=new TlsTransport(port,host_ipaddr,nmax_connections,key_file,cert_file,trust_folder,trust_all);
            }
            if (transp!=null)
            {  setTransport(transp);
            }
        } catch (Exception e) {
            e.printStackTrace();
            }
        }
    }


    /** Stops the transport services. */
    private void stopSipTrasport()
    {  if (sip_transports!=null)
    {  for(Enumeration e=sip_transports.keys(); e.hasMoreElements(); )
    {  String proto=(String)e.nextElement();
        Transport transp=(Transport)sip_transports.get(proto);
        Log.v(TAG, proto + " is going down");
        transp.halt();
    }
        sip_transports.clear();
        sip_transports=null;
    }
    }


    /** Sets a specific transport protocol. */
    public void setTransport(Transport transport) {
        String proto = transport.getProtocol();
        removeTransport(proto);
        sip_transports.put(proto,transport);
        transport.setListener(this);
        if(default_transport == null)
            default_transport = proto;
//        Log.v(TAG, proto + " is up at port " + transport.getLocalPort());
    }

    /** Removes a specific transport protocol. */
    public void removeTransport(String proto)
    {  if (sip_transports.containsKey(proto))
    {  Transport t=(Transport)sip_transports.get(proto);
        sip_transports.remove(proto);
        t.halt();
        if (proto.equals(default_transport)) default_transport=null;
//        Log.v(TAG, proto+" is down");
    }
    }


    /** Stops the SipProviders. */
    public synchronized void halt() {
//        Log.v(TAG, "halt: SipProvider is going down");
        stopSipTrasport();
        sip_listeners=new Hashtable();
        promiscuous_listeners=new Vector();
        exception_listeners=new Vector();
    }

    public void readAll(Context context) {

        SharedPreferences prefs = context.getSharedPreferences("SipStack", Context.MODE_PRIVATE);

        via_addr = prefs.getString("via_addr", via_addr);
        host_port = prefs.getInt("host_port", host_port);
        host_ifaddr = prefs.getString("host_ifaddr", host_ifaddr);

        String tp = prefs.getString("transport_protocols", transport_protocols == null ? null : TextUtils.join(",", transport_protocols));
        if(tp == null) transport_protocols = null;
        else transport_protocols = tp.split(",");

        if(transport_ports != null) {
            String[] stpotrs = new String[transport_ports.length];
            for (int i = 0; i < transport_ports.length; i++) {
                stpotrs[i] = ""+transport_ports[i];
            }
            String tports = prefs.getString("transport_ports", TextUtils.join(",", stpotrs));
            stpotrs = tports.split(",");
            transport_ports = new int[stpotrs.length];
            for (int i = 0; i < transport_ports.length; i++) {
                transport_ports[i] = Integer.parseInt(stpotrs[i]);
            }
        } else {
            String tports = prefs.getString("transport_ports", null);
            if(tports == null) transport_ports = null;
            else {
                String [] stpotrs = tports.split(",");
                transport_ports = new int[stpotrs.length];
                for (int i = 0; i < transport_ports.length; i++) {
                    transport_ports[i] = Integer.parseInt(stpotrs[i]);
                }
            }
        }

        nmax_connections = prefs.getInt("nmax_connections", nmax_connections);
        String url = prefs.getString("outbound_proxy", null);
        if (url==null || url.length()==0 || url.equalsIgnoreCase("NONE") || url.equalsIgnoreCase("NO-OUTBOUND"))
            outbound_proxy = null;
        else
            outbound_proxy = new SipURL(url);

        log_all_packets = prefs.getBoolean("log_all_packets", log_all_packets);

        // certificates
        trust_all = prefs.getBoolean("trust_all", trust_all);
        trust_folder = prefs.getString("trust_folder", trust_folder);
        cert_file = prefs.getString("cert_file", cert_file);
        key_file = prefs.getString("key_file", key_file);

    }


    /** Gets a String with the list of transport protocols. */
    private String transportProtocolsToString()
    {  if (sip_transports==null) return "";
        // else
        StringBuffer sb=new StringBuffer();
        for (Enumeration e=sip_transports.keys(); e.hasMoreElements(); )
        {  sb.append("/").append((String)e.nextElement());
        }
        return sb.toString();
    }


    // ************************** Public methods *************************

    /** Whether it supports <i>proto</i> as transport protocol. */
    public boolean hasTransport(String proto)
    {  if (sip_transports!=null) return sip_transports.containsKey(proto.toLowerCase());
    else return false;
    }

    /** Gets via address. */
    public String getViaAddress()
    {  return via_addr;
    }

    /** Sets via address. */
   /*public void setViaAddress(String addr)
   {  via_addr=addr;
   }*/

    /** Gets host port. */
    public int getPort()
    {  return host_port;
    }

    /** Gets tls port. */
    public int getTlsPort()
    {  return (sip_transports.containsKey(PROTO_TLS))? ((Transport)sip_transports.get(PROTO_TLS)).getLocalPort() : 0;
    }

    /** Gets a valid contact address with user rtpmap and transport information. */
    public SipURL getContactAddress(String user) {
        SipURL url = (getPort() != SipStack.default_port) ? new SipURL(user, getViaAddress(), getPort()) : new SipURL(user, getViaAddress());
        if(!hasTransport(PROTO_UDP))
            url.addTransport(getDefaultTransport());
        return url;
    }

    /** Gets a valid secure contact address with user rtpmap and transport information. */
    public SipURL getSecureContactAddress(String user)
    {  if (hasTransport(SipProvider.PROTO_TLS))
    {  SipURL url=(getTlsPort()!=SipStack.default_tls_port)? new SipURL(user,getViaAddress(),getTlsPort()) : new SipURL(user,getViaAddress());
        url.setSecure(true);
        return url;
    }
    else return null;
    }

    /** Whether binding the sip provider to all interfaces or only on the specified host address. */
    public boolean isAllInterfaces()
    {  return host_ipaddr==null;
    }

    /** Gets host interface IpAddress. */
    public IpAddress getInterfaceAddress()
    {  return host_ipaddr;
    }

    /** Gets array of transport protocols. */
    public String[] getTransportProtocols()
    {  String[] protocols=new String[sip_transports.size()];
        Enumeration e=sip_transports.keys();
        for (int i=0; i<protocols.length; i++) protocols[i]=(String)e.nextElement();
        return protocols;
    }

    /** Whether the given transport protocol is supported. */
    public boolean isSupportedTransport(String proto)
    {  return sip_transports.containsKey(proto.toLowerCase());
    }

    /** Whether the given transport protocol is supported and reliable. */
    public boolean isReliableTransport(String proto)
    {  return isReliableTransport((Transport)sip_transports.get(proto.toLowerCase()));
    }

    /** Whether the given transport is reliable. */
    boolean isReliableTransport(Transport transp)
    {  if (transp!=null) try {  return Class.forName("org.zoolu.sip.provider.ConnectedTransport").isInstance(transp);  } catch (ClassNotFoundException e) {}
        // else
        return false;
    }

    /** Gets the default transport protocol. */
    public String getDefaultTransport()
    {  return default_transport;
    }

    /** Gets the default transport protocol. */
    public synchronized void setDefaultTransport(String proto)
    {  default_transport=proto;
    }

    /** Sets rport support. */
    public synchronized void setRport(boolean flag)
    {  rport=flag;
    }

    /** Whether using rport. */
    public boolean isRportSet()
    {  return rport;
    }

    /** Sets 'force-rport' mode. */
    public synchronized void setForceRport(boolean flag)
    {  force_rport=flag;
    }

    /** Whether using 'force-rport' mode. */
    public boolean isForceRportSet()
    {  return force_rport;
    }

    /** Whether has outbound proxy. */
    public boolean hasOutboundProxy()
    {  return outbound_proxy!=null;
    }

    /** Gets the outbound proxy. */
    public SipURL getOutboundProxy()
    {  return outbound_proxy;
    }

    /** Sets the outbound proxy. Use 'null' for not using any outbound proxy. */
    public synchronized void setOutboundProxy(SipURL url)
    {  outbound_proxy=url;
    }

    /** Removes the outbound proxy. */
   /*public void removeOutboundProxy()
   {  setOutboundProxy(null);
   }*/

    /** Gets the max number of (contemporary) open connections. */
    public int getNMaxConnections()
    {  return nmax_connections;
    }

    /** Sets the max number of (contemporary) open connections. */
    public synchronized void setNMaxConnections(int n)
    {  nmax_connections=n;
    }

    /** Returns the table of active listeners as Hastable:(SipId)IDs-->(SipListener)listener. */
    public Hashtable getListeners()
    {  return sip_listeners;
    }


    /** Sets a SipProvider listener for a target type of method, transaction, or dialog messages.
     * @param id is the identifier that specifies the messages that the listener
     * as to be associated to. It may identify a method, a transaction, or a dialog, or all messages.
     * Use SipProvider.ANY to capture all messages.
     * @param listener is the SipProviderListener that the specified type of messages has to be passed to. */
    public synchronized void addSelectiveListener(SipId id, SipProviderListener listener) {
//        Log.v(TAG, "setting SipProviderListener: "+id);
        SipId key=id;
        if (sip_listeners.containsKey(key))
        {  Log.v(TAG,"setting a SipProvider listener with an identifier already selected: the previous listener is removed.");
            sip_listeners.remove(key);
        }
        sip_listeners.put(key,listener);
//        Log.v(TAG, "active sip listeners: "+sip_listeners.size());
    }


    /** Removes a SipProviderListener.
     * @param id is the identifier that specifies the messages that the listener was associated to. */
    public synchronized void removeSelectiveListener(SipId id) {
//        Log.v(TAG, "removing SipProviderListener: "+id);
        SipId key=id;
        if (!sip_listeners.containsKey(key)) {
//            Log.v(TAG,"removeListener("+id+"): no such listener found.");
        }
        else
        {  sip_listeners.remove(key);
        }
//        Log.v(TAG, "active sip listeners: "+sip_listeners.size());
    }


    /** Adds a SipProvider listener for caputering any message in promiscuous mode.
     * <p/>
     * When a SipProviderListener captures messages in promiscuous mode
     * messages are passed to this listener before passing them to other specific listener.
     * <br/> More that one SipProviderListener can be active in promiscuous mode at the same time;
     * in that case the same message is passed to all promiscuous SipProviderListeners.
     * @param listener is the SipProviderListener. */
    public synchronized void addPromiscuousListener(SipProviderListener listener) {
//        Log.v(TAG, "adding SipProviderListener in promiscuous mode");
        if (promiscuous_listeners.contains(listener)) {
//            Log.v(TAG,"trying to add an already present SipProviderListener in promiscuous mode.");
        }
        else
        {  promiscuous_listeners.addElement(listener);
        }
    }


    /** Removes a SipProviderListener in promiscuous mode.
     * @param listener is the SipProviderListener to be removed. */
    public synchronized void removePromiscuousListener(SipProviderListener listener) {
//        Log.v(TAG, "removing SipProviderListener in promiscuous mode");
        if (!promiscuous_listeners.contains(listener)) {
//            Log.v(TAG,"trying to remove a missed SipProviderListener in promiscuous mode.");
        }
        else
        {  promiscuous_listeners.removeElement(listener);
        }
    }


    /** Adds a SipProviderExceptionListener.
     * The SipProviderExceptionListener is a listener for all exceptions thrown by the SipProviders.
     * @param listener is the SipProviderExceptionListener. */
    public synchronized void addExceptionListener(SipProviderExceptionListener listener) {
//        Log.v(TAG, "adding a SipProviderExceptionListener");
        if (exception_listeners.contains(listener)) {
//            Log.v(TAG,"trying to add an already present SipProviderExceptionListener.");
        }
        else
        {  exception_listeners.addElement(listener);
        }
    }


    /** Removes a SipProviderExceptionListener.
     * @param listener is the SipProviderExceptionListener to be removed. */
    public synchronized void removeExceptionListener(SipProviderExceptionListener listener) {
//        Log.v(TAG, "removing a SipProviderExceptionListener");
        if (!exception_listeners.contains(listener)) {
//            Log.v(TAG,"trying to remove a missed SipProviderExceptionListener.");
        }
        else
        {  exception_listeners.removeElement(listener);
        }
    }


    /** Sends the message <i>msg</i>.
     * <p/>
     * The destination for the request is computed as follows:
     * <br/> - if <i>outbound_addr</i> is set, <i>outbound_addr</i> and 
     *        <i>outbound_port</i> are used, otherwise
     * <br/> - if message has Route header with lr option parameter (i.e. RFC3261 compliant),
     *        the first Route address is used, otherwise
     * <br/> - the request's Request-URI is considered.
     * <p/>
     * The destination for the response is computed based on the sent-by parameter in
     *     the Via header field (RFC3261 compliant)
     * <p/>
     * As transport it is used the protocol specified in the 'via' header field 
     * <p/>
     * In case of connection-oriented transport:
     * <br/> - if an already established connection is found matching the destination
     *        end point (socket), such connection is used, otherwise
     * <br/> - a new connection is established
     *
     * @return Returns a TransportConnId in case of connection-oriented delivery
     * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP)
     */
    public TransportConnId sendMessage(Message msg) {
//        Log.v(TAG, "Sending message:\r\n"+msg.toString());

        // select the transport protocol
        ViaHeader via=msg.getViaHeader();
        String via_proto=via.getProtocol().toLowerCase();
        String proto=via_proto;

        // select the destination address and port
        String dest_addr=null;
        int dest_port=0;
        int ttl=0;

        if (msg.isRequest())
        {  // REQUESTS
            SipURL url=outbound_proxy;
            // else
            if (url==null)
            {  if (msg.hasRouteHeader())
            {  SipURL route_url=msg.getRouteHeader().getNameAddress().getAddress();
                if (route_url.hasLr()) url=msg.getRouteHeader().getNameAddress().getAddress();
            }
            }
            // else
            if (url==null)
            {  url=msg.getRequestLine().getAddress();
            }
            dest_addr=url.getHost();
            dest_port=url.getPort();
            if (url.isSecure()) proto="tls";
            else if (url.hasTransport()) proto=url.getTransport();
            // if maddr is set, update the via header by adding maddr and ttl params
            if (url.hasMaddr())
            {  dest_addr=url.getMaddr();
                if (url.hasTtl()) ttl=url.getTtl();
                via.setMaddr(dest_addr);
                if (ttl>0) via.setTtl(ttl);
                msg.removeViaHeader();
                msg.addViaHeader(via);
            }
            // if the resulting proto differs from via proto, update via
            if (!via_proto.equalsIgnoreCase(proto))
            {  via.setProtocol(proto);
                msg.removeViaHeader();
                msg.addViaHeader(via);
            }
//            Log.v(TAG, "using transport "+proto);
        }
        else
        {  // RESPONSES
            SipURL url=via.getSipURL();
            if (via.hasReceived()) dest_addr=via.getReceived(); else dest_addr=url.getHost();
            if (via.hasRport()) dest_port=via.getRport();
            if (dest_port<=0) dest_port=url.getPort();
        }

        if (dest_port<=0)
        {  if (proto.equalsIgnoreCase("tls")) dest_port=SipStack.default_tls_port;
        else dest_port=SipStack.default_port;
        }

        return sendMessage(msg,proto,dest_addr,dest_port,ttl);
    }


    /** Sends a Message, specifing the transport portocol, nexthop address and port.
     * <p/>
     * This is a low level method and forces the message to be routed to
     * a specific nexthop address, port and transport,
     * regardless whatever the Via, Route, or request-uri, address to. 
     * <p/>
     * In case of connection-oriented transport, the connection is selected as follows:
     * <br/> - if an existing connection is found matching the destination
     *        end point (destination socket), such connection is used, otherwise
     * <br/> - a new connection is established.
     *
     * @return It returns a Connection in case of connection-oriented delivery
     * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP)
     */
    public TransportConnId sendMessage(Message msg, String proto, String dest_addr, int dest_port, int ttl)
    {
//        if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) Log.v(TAG, "Resolving host address '"+dest_addr+"'");
        try
        {  IpAddress dest_ipaddr=IpAddress.getByName(dest_addr);
            return sendMessage(msg,proto,dest_ipaddr,dest_port,ttl);
        }
        catch (Exception e)
        {  Log.e(TAG,"",e);
            return null;
        }
    }

    /** Sends a Message, specifing the transport portocol, nexthop address and port. */
    private TransportConnId sendMessage(Message msg, String proto, IpAddress dest_ipaddr, int dest_port, int ttl)
    {
//        if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) Log.v(TAG, "Sending message to "+(new TransportConnId(proto,dest_ipaddr,dest_port)).toString());

        TransportConn conn=null;
        try
        {  Transport transp=(Transport)sip_transports.get(proto.toLowerCase());
            if (transp!=null)
            {  conn=transp.sendMessage(msg,dest_ipaddr,dest_port,ttl);
            }
            else
            {
//                Log.v(TAG,"Unsupported protocol ("+proto+"): Message discarded");
                return null;
            }
        }
        catch (IOException e)
        {  Log.e(TAG,"",e);
            return null;
        }
        // logs
        Log.v(SDP_TAG, "Message sent:\n\n"+msg.toString()+"\n");

        if (conn!=null) return new TransportConnId(conn);
        else return null;
    }


    /** Sends the message <i>msg</i> using the specified transport connection. */
    public TransportConnId sendMessage(Message msg, TransportConnId conn_id)
    {
//        if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) Log.v(TAG, "Sending message through conn "+conn_id);
//        Log.v(TAG, "message:\r\n"+msg.toString());
        TransportConn conn=null;
        for (Enumeration e=sip_transports.elements(); e.hasMoreElements() && conn==null; )
        {  Transport transp=(Transport)e.nextElement();
            if (isReliableTransport(transp)) conn=((ConnectedTransport)transp).sendMessage(msg,conn_id);
        }
        if (conn!=null)
        {  // logs
            Log.v(SDP_TAG, "Message sent:\n\n"+msg.toString()+"\n");

            return new TransportConnId(conn);
        }
        else
        {  return sendMessage(msg);
        }
    }


    //************************* Callback methods *************************

    /** From TransportListener. When a new SIP message is received. */
    public synchronized void onReceivedMessage(Transport transport, Message msg)
    {  try
    {  // logs
        Log.v(TAG, "Received message:\n" + msg.toString());

        // discard too short messages
        if (msg.getLength()<=2)
        {  if (log_all_packets) Log.v(TAG, "message too short: discarded\r\n");
            return;
        }
        // discard non-SIP messages
        String first_line=msg.getFirstLine();
        if (first_line==null || first_line.toUpperCase().indexOf("SIP/2.0")<0)
        {  if (log_all_packets) Log.v(TAG, "NOT a SIP message: discarded\r\n");
            return;
        }

        Log.v(SDP_TAG, "Received message:\n\n"+msg.toString()+"\n");

        // if a request, handle "received" and "rport" parameters
        if (msg.isRequest())
        {  ViaHeader vh=msg.getViaHeader();
            boolean via_changed=false;
            String src_addr=msg.getRemoteAddress();
            int src_port=msg.getRemotePort();
            String via_addr=vh.getHost();
            int via_port=vh.getPort();
            if (via_port<=0) via_port=SipStack.default_port;

            if (!via_addr.equals(src_addr))
            {  vh.setReceived(src_addr);
                via_changed=true;
            }

            if (vh.hasRport())
            {  vh.setRport(src_port);
                via_changed=true;
            }
            else
            {  if (force_rport && via_port!=src_port)
            {  vh.setRport(src_port);
                via_changed=true;
            }
            }

            if (via_changed)
            {  msg.removeViaHeader();
                msg.addViaHeader(vh);
            }
        }

        // is there any listeners?
        if (sip_listeners.size()==0)
        {  Log.v(TAG, "no listener found: message discarded.");
            return;
        }

        // try to look for listeners in promiscuous mode
        for (int i=0; i<promiscuous_listeners.size(); i++)
        {  SipProviderListener listener=(SipProviderListener)promiscuous_listeners.elementAt(i);
//            Log.v(TAG, "message passed to promiscuous listener");
            listener.onReceivedMessage(this,msg);
        }

        // check if the message is still valid
        if (!msg.isRequest() && !msg.isResponse())
        {  Log.v(TAG, "no valid SIP message: message discarded.");
            Log.v(TAG, "message:\r\n"+msg.toString());
            return;
        }

        // look for a specifid listener:

        // try to look for a transaction
        SipId key;
        // pass requests to transaction servers and response to transaction clients
        if (msg.isRequest()) key=msg.getTransactionServerId(); else key=msg.getTransactionClientId();
//        Log.v(TAG, "transaction-id: "+key);
        if (sip_listeners.containsKey(key)) {
//            Log.v(TAG, "message passed to transaction: "+key);
            SipProviderListener lis = (SipProviderListener) sip_listeners.get(key);
            if(lis != null) {
//                Log.v(TAG, lis.getClass().toString());
                lis.onReceivedMessage(this, msg);
            }
            return;
        }
        // try to look for a dialog
        key=msg.getDialogId();
//        Log.v(TAG, "dialog-id: "+key);
        if (sip_listeners.containsKey(key))
        {
//            Log.v(TAG, "message passed to dialog: "+key);
            ((SipProviderListener)sip_listeners.get(key)).onReceivedMessage(this,msg);
            return;
        }
        // try to look for a UAS
        key=msg.getMethodId();
        if (sip_listeners.containsKey(key))
        {
//            Log.v(TAG, "message passed to uas: "+key);
            ((SipProviderListener)sip_listeners.get(key)).onReceivedMessage(this,msg);
            return;
        }
        // try to look for a default MyUA
        if (sip_listeners.containsKey(ANY))
        {  Log.v(TAG, "message passed to uas: "+ANY);
            ((SipProviderListener)sip_listeners.get(ANY)).onReceivedMessage(this,msg);
            return;
        }

        // if we are here, no listener_ID matched..
        Log.v(TAG, "no listener found matching that message: message discarded.");
        //Log.v(TAG, "Pending SipProviderListeners= "+getListeners().size(),3);
//        Log.v(TAG, "active listeners: "+sip_listeners.size());
    }
    catch (Exception exception)
    {  Log.v(TAG,"Error handling a new incoming message:\n"+msg.toString());
        Log.e(TAG,"",exception);
        for (int i=0; i<exception_listeners.size(); i++)
        {  try
        {  ((SipProviderExceptionListener)exception_listeners.elementAt(i)).onMessageException(msg,exception);
        }
        catch (Exception e)
        {  Log.v(TAG,"Error handling the Exception");
            Log.e(TAG,"",e);
        }
        }
    }
    }


    /** From TransportListener. When Transport terminates. */
    public void onTransportTerminated(Transport transport, Exception error)
    {  Log.v(TAG, "transport "+transport+" terminated");
        // TRY TO RESTART UDP WHEN ERRORS OCCUR
        if (error!=null && transport.getProtocol().equals(PROTO_UDP))
        {  Log.v(TAG, "transport UDP terminated with error: trying to restart it (after 1000ms)..");
            try {  Thread.sleep(1000);  } catch (Exception e) {}
            try
            {  Transport udp=new UdpTransport(host_port,host_ipaddr);
                setTransport(udp);
            }
            catch (Exception e)
            {  Log.e(TAG,"",e);
            }
        }
    }


    //************************** Other methods ***************************

    /** Picks a fresh branch value.
     * The branch ID MUST be unique across space and time for
     * all requests sent by the MyUA.
     * The branch ID always begin with the characters "z9hG4bK". These
     * 7 characters are used by RFC 3261 as a magic cookie. */
    public static String pickBranch()
    {  //String str=Long.toString(Math.abs(Random.nextLong()),16);
        //if (str.length()<5) str+="00000";
        //return "z9hG4bK"+str.substring(0,5);
        return "z9hG4bK"+Random.nextHexString(8);
    }

    /** Picks an unique branch value based on a SIP message.
     * This value could also be used as transaction ID */
    public String pickBranch(Message msg)
    {  StringBuffer sb=new StringBuffer();
        sb.append(msg.getRequestLine().getAddress().toString());
        sb.append(getViaAddress()+getPort());
        ViaHeader top_via=msg.getViaHeader();
        if (top_via.hasBranch())
            sb.append(top_via.getBranch());
        else
        {  sb.append(top_via.getHost()+top_via.getPort());
            sb.append(msg.getCSeqHeader().getSequenceNumber());
            sb.append(msg.getCallIdHeader().getCallId());
            sb.append(msg.getFromHeader().getTag());
            sb.append(msg.getToHeader().getTag());
        }
        //return "z9hG4bK"+(new MD5(unique_str)).asHex().substring(0,9);
        return "z9hG4bK"+(new SimpleDigest(5,sb.toString())).asHex();
    }


    /** Picks a new tag.
     * A tag  MUST be globally unique and cryptographically random
     * with at least 32 bits of randomness.  A property of this selection
     * requirement is that a MyUA will place a different tag into the From
     * header of an INVITE than it would place into the To header of the
     * response to the same INVITE.  This is needed in order for a MyUA to
     * invite itself to a session. */
    public static String pickTag()
    {  //String str=Long.toString(Math.abs(Random.nextLong()),16);
        //if (str.length()<8) str+="00000000";
        //return str.substring(0,8);
        return Random.nextNumString(12);
    }

    /** Picks a new tag. The tag is generated uniquely based on message <i>req</i>.
     * This tag can be generated for responses in a stateless
     * manner - in a manner that will generate the same tag for the
     * same request consistently.
     */
    public static String pickTag(Message req)
    {  //return String.valueOf(tag_generator++);
        //return (new MD5(request.toString())).asHex().substring(0,8);
        return (new SimpleDigest(8,req.toString())).asHex();
    }


    /** Picks a new call-id.
     * The call-id is a globally unique identifier over space and time.
     * It is implemented in the form "localid@host".
     * Call-id must be considered case-sensitive and is compared byte-by-byte. */
    public String pickCallId()
    {  return pickCallId(getViaAddress());
    }


    /** Picks a new call-id.
     * The call-id is a globally unique
     * identifier over space and time. It is implemented in the
     * form "localid@host". Call-id must be considered case-sensitive and is
     * compared byte-by-byte. */
    public static String pickCallId(String hostaddr)
    {  //String str=Long.toString(Math.abs(Random.nextLong()),16);
        //if (str.length()<12) str+="000000000000";
        //return str.substring(0,12)+"@"+hostaddr();
        return Random.nextNumString(12)+"@"+hostaddr;
    }


    /** picks an initial CSeq */
    public static int pickInitialCSeq()
    {  return 1;
    }


    /** (<b>Deprecated</b>) Constructs a NameAddress based on an input string.
     * The input string can be a:
     * <br/> - <i>user</i> rtpmap,
     * <br/> - <i>user@address</i> url,
     * <br/> - <i>"Name" &lt;sip:user@address&gt;</i> address,
     * <p/>
     * In the former case,
     * a SIP URL is costructed using the outbound proxy as host address if present,
     * otherwise the local via address is used. */
   /*public NameAddress completeNameAddress(String str)
   {  if (str.indexOf("<sip:")>=0) return new NameAddress(str);
      else
      {  SipURL url=completeSipURL(str);
         return new NameAddress(url);
      }
   }*/
    /** Constructs a SipURL based on an input string. */
   /*private SipURL completeSipURL(String str)
   {  // in case it is passed only the 'user' field, add '@'<outbound_proxy>[':'<outbound_port>]
      if (!str.startsWith("sip:") && !str.startsWith("sips:") && str.indexOf("@")<0 && str.indexOf(".")<0 && str.indexOf(":")<0)
      {  // probably it is just the user rtpmap..
         if (outbound_proxy!=null)
         {  String host=outbound_proxy.getHost();
            int port=outbound_proxy.getPort();
            SipURL url=(port>0 && port!=SipStack.default_port)? new SipURL(str,host,port) : new SipURL(str,host);
            if (outbound_proxy.isSecure()) url.setSecure(true);
            return url;
         }
         else
         {  SipURL url=(host_port>0 && host_port!=SipStack.default_port)? new SipURL(str,via_addr,host_port) : new SipURL(str,via_addr);
            if (transport_protocols[0].equals(PROTO_TLS)) url.setSecure(true);
            return url;
         }
      }
      else return new SipURL(str);
   }*/


    /** Gets a String value for this object. */
    public String toString()
    {  if (host_ipaddr==null) return host_port+"/"+transportProtocolsToString();
    else return host_ipaddr.toString()+":"+host_port+"/"+transportProtocolsToString();
    }

}
